Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

44.Gün - SwiftUI Navigation: Programmatic Navigation ve Path Kaydetme

Programmatic Navigation, kullanıcının herhangi bir eylemde bulunmasını beklemeden, programsal olarak tetiklenerek gerçekleşir. Örneğin bazı veriler işlendikten sonra kullancıyı sonuç ekranına götürebiliriz.

Bu SwiftUI’de NavigationStack path’ini veriye bind ederek gerçekleştirilir.

struct ContentView: View {
    @State private var path = [Int]()

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                // more code to come
            }
            .navigationDestination(for: Int.self) { selection in
                Text("You selected \(selection)")
            }
        }
    }
}

Buradaki // more code to come kısmına iki adet buton ekleyelim;

Button("Show 32") {
    path = [32]
}

Button("Show 64") {
    path.append(64)
}

İlk buton ile tüm array’i değiştirerek sadece 32 sayısını içerecek şekilde ayarlıyoruz. Eğer array’de başka bir şey varsa kaldırılacaktır, yani NavigationStack 32 sayısına gitmeden önce orijinal durumuna geri dönecektir.

İkinci buton ile mevcut array’e 64 sayısını ekliyoruz, yani bu sayı gitmekte olduğumuz şeye eklenecektir. Dolayısıyla, array zaten 32 içeriyorsa, artık stack’de üç view olacaktır: Orijinal view (”root” olarak adlandırılır), ardından 32 sayısını gösteren bir şey ve son olarak 64 sayısını gösteren bir şey.

Aynı anda birden fazla değeri de push edebilirsiniz, bunun gibi;

Button("Show 32 then 64") {
    path = [32, 64]
}

Bu, 32 için bir view ardından 64 için bir view sunacaktır, bu nedenle kullanıcının root view’e geri dönmek için iki kez Back butonuna dokunması gerekir.

SwiftUI NavigationStack

Farklı veri türleri ile navigation iki şekilde gerçekleşir. En basit olanı, navigationDestination() methodunu kullanarak farklı veri türlerini kullandığımız ancak gösterilen yolu tam olarak takip etmediğimiz durumdur, çünkü burada işler basittir: navigationDestination() methodun istediğimiz her veri türü için olmak üzere çoklu olarak ekleyebiliriz.

Örneğin, beş sayı ve beş string gösterebilir ve bunlara farklı şekilde gidebiliriz.

NavigationStack {
    List {
        ForEach(0..<5) { i in
            NavigationLink("Select Number: \(i)", value: i)
        }

        ForEach(0..<5) { i in
            NavigationLink("Select String: \(i)", value: String(i))
        }
    }
    .navigationDestination(for: Int.self) { selection in
        Text("You selected the number \(selection)")
    }
    .navigationDestination(for: String.self) { selection in
        Text("You selected the string \(selection)")
    }
}

Ancak, programmatic navigation eklemek istediğimizde işler daha karmaşık hale gelir, çünkü navigation stack path’e bazı verileri bind etmemiz gerekir. path değişkenine basit veri türlerini eklemeyi daha önce görmüştük, fakat daha kompleks veri türlerini işler biraz değişiyor.

SwiftUI’nin çözümü, tek bir path’de çeşitli veri türlerini tutabilen NavigationPath adlı özel bir türdür. Pratikte bir array’e çok benzer şekilde çalışır.

Şu şekilde bir path değişkeni oluşturabiliriz;

@State private var path = NavigationPath()

Şu şekilde NavigationStack’e bind edelim;

NavigationStack(path: $path) {

Örnek olarak toolbar butonları ile programmatic olarak bir şeyler gösterelim;

.toolbar {
    Button("Push 556") {
        path.append(556)
    }

    Button("Push Hello") {
        path.append("Hello")
    }
}

Bir NavigationStack’te bir kaç seviye derine indikten sonra başa dönmek isteyebiliriz. Örneğin, belki de kullanıcınız bir sipariş veriyordur ve sepetini gösteren, kargo bilgilerini, ödeme bilgilerini isteyen, ardından siparişi onaylayan ekranlar arasında ilerlemiştir, ancak iş bittiğinde en başa, NavigationStack’in root view’ına geri dönmek isteyebilir.

Bunu göstermek içini her seferinde yeni rastgele sayılar üreterek yeni view’ları sonsuza kadar push eden küçük bir sanal alan oluşturabiliriz.

İlk olarak, mevcut numarasını başlık olarak gösteren ve her basıldığında yeni bir rastgele numaraya giden bir butona sahip DetailView’ımızı oluşturalım;

struct DetailView: View {
    var number: Int

    var body: some View {
        NavigationLink("Go to Random Number", value: Int.random(in: 1...1000))
            .navigationTitle("Number: \(number)")
    }
}

Ve şimdi bunu ContentView’ımızdan sunabailiriz, başlangıç değeri 0 ile başlar ancak her yeni Int gösterildiğinde yeni bir DetailView’a gider:

struct ContentView: View {
    @State private var path = [Int]()

    var body: some View {
        NavigationStack(path: $path) {
            DetailView(number: 0)
                .navigationDestination(for: Int.self) { i in
                    DetailView(number: i)
                }
        }
    }
}

Bunu çalıştırdığınızda, view’lar arasında sonsuza kadar ilerlemeye devam edebileceğinizi göreceksiniz.

Örneğin 10 view derine gittiyseniz root’a dönmek istediğinizde iki seçeneğimiz var;

  1. Yukarıdaki kodda yaptığımız gibi path için basit bir array kullanıyorsanız, path’deki her şeyi kaldırmak için bu array üzerinde removeAll() işlevini çağırabilir ve root view’a geri dönebilirsiniz.
  2. path için NavigationPath kullanıyorsanız, bunu NavigationPath’in yeni, boş bir instance’ını yaparak halledebiliriz. Şu şekilde : path = NavigationPath()

Ancak daha büyük bir sorun var: orijinal path property’ye erişimimiz olmadığında bunu alt view’dan nasıl yapabiliriz?

Burada iki seçeneğimiz var: path’i @Observable kullanan harici bir sınıfta saklamak ya da @Binding adında yeni bir property wrapper kullanmak. Daha önce @Observable’ı incelemiştik, o yüzden burada @Binding’e odaklanalım.

Uygulamamız çalışırken değerleri değiştirebilmemiz için @State’in view’ın içinde bir depolama alanı oluşturmamıza nasıl izin verdiğini gördük. @Binding property wrapper, bir @State property’yi başka bir view’a aktarmamıza ve oradan değiştirmemize olanak tanır. Yani @State property’yi birkaç yerde paylaşabiliriz ve bir yerde değiştirmek onu her yerde değiştirir.

Mevcut kodumuzda bu, navigation path array’e erişmek için DetailView’e yeni bir property eklemek anlamına geliyor.

@Binding var path: [Int]

Ve şimdi bunu ContentView’da DetailView’in kullanıldığı her iki yerden de şu şekilde geçmemiz gerekiyor.

DetailView(number: 0, path: $path)
    .navigationDestination(for: Int.self) { i in
        DetailView(number: i, path: $path)
    }

Gördüğünüz gibi $path değişkenini geçiyoruz çünkü binding yapmak istiyoruz. Yani DetailView’in path’i okuyup yazabilmesini istiyoruz.

Ve şimdi path array’i değiştirmek için DetailView’e bir toolbar ekleyebiliriz.

.toolbar {
    Button("Home") {
        path.removeAll()
    }
}

Ve tabiki NavigationPathkullanıyorsanız bunu kullanırsınız;

.toolbar {
    Button("Home") {
        path = NavigationPath()
    }
}

Bu şekilde binding yapmak yaygındır. TextField, Stepper gibi kontroller tam olarak bu şekilde çalışır.

Codable Kullanarak NavigationStack Path Nasıl Kaydedilir? #

Codable kullanarak navigation stack path’i 2 farklı yoldan biriyle kaydedebilir ve yükleyebiliriz. Bu seçim path’in türüne bağlıdır.

  1. NavigationStack’imizin aktif path’ini saklamak için NavigationPath kullanıyorsanız, SwiftUI yollarınızı kaydetmeyi ve yüklemeyi kolaylaştırmak için iki yardımcı sağlar.
  2. Homojen bir array kullanıyorsanız örneğin [Int] veya [String], bu yardımcılara ihtiyacınız yoktur ve verilerinizi özgürce yükleyebilir veya kaydedebilirsiniz.

Teknikler çok benzer, bu yüzden burada ikisini de ele alacağız.

Her ikisi de path’i view dışında depolamaya dayanır, böylece path verilerinin yüklenmesi ve kaydedilmesi görünmez bir şekilde gerçekleşir, yani external bir sınıf bunu otomatik olarak halleder. Daha spesifik olmak gerekirse, path verilerimiz değiştiğinde (int, string veya NavigationPath nesnesi) yeni path’i kaydetmemiz gerekir, böylece gelecekte saklanır ve sınıf başlatıldığında bu verileri diskten geri yükleriz.

Path verilerimiz bir Int array olarak saklandığında bu sınıfın nasıl görüneceği aşağıda açıklanmıştır;

@Observable
class PathStore {
    var path: [Int] {
        didSet {
            save()
        }
    }

    private let savePath = URL.documentsDirectory.appending(path: "SavedPath")

    init() {
        if let data = try? Data(contentsOf: savePath) {
            if let decoded = try? JSONDecoder().decode([Int].self, from: data) {
                path = decoded
                return
            }
        }

        // Still here? Start with an empty path.
        path = []
    }

    func save() {
        do {
            let data = try JSONEncoder().encode(path)
            try data.write(to: savePath)
        } catch {
            print("Failed to save navigation data")
        }
    }
}

NavigationPath kullanıyorsanız, yalnızca dört küçük değişikliğe ihtiyacınız vardır.

İlk olarak, path property’nin [Int]yerine NavigationPath türüne sahip olması gerekir.

var path: NavigationPath {
    didSet {
        save()
    }
}

İkinci olarak, initializer’da JSON decode ettiğimiz yerde gerekli değişikliği yapmamız gerekiyor.

if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
    path = NavigationPath(decoded)
    return
}

Üçüncü olarak, decode işlemi başarısız olursa, initializer’ın sonundaki path property yeni boş bir NavigationPath instance atamalıyız;

path = NavigationPath()

Ve son olarak, save() methodu navigation path’in Codable temsilini yazması gerekir. Burada basit bir array kullanmaktan biraz daha fazlasını yapmalıyız, çünkü NavigationPath veri tiplerinin Codable’a uygun olmasını gerektirmiyor (sadece Hashable’a uygunluğuna ihtiyaç duyuyor). Sonuç olarak, Swift derleme zamanında navigation path’i geçerli bir Codable temsili olduğunu doğrulayamaz, bu nedenle bunu talep etmemiz ve ne geri geleceğini görmemiz gerekir.

Bu da save() methodunun başına, Codable navigation path’i almaya çalışan ve geri dönüş alamazsak hemen iptal eden bir kontrol eklemek anlamına geliyor.

guard let representation = path.codable else { return }

Bu, JSON’a encode edilmeye hazır verileri döndürür ya da path’deki en az bir nesne encode edilemezse nil döndürür.

Son olarak, bu Codable gösterimini orijinal Int dizisi yerine JSON’a dönüştürüyoruz.

İşte sınıfın tamamlanmış hali böyle gözüküyor.

@Observable
class PathStore {
    var path: NavigationPath {
        didSet {
            save()
        }
    }

    private let savePath = URL.documentsDirectory.appending(path: "SavedPath")

    init() {
        if let data = try? Data(contentsOf: savePath) {
            if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
                path = NavigationPath(decoded)
                return
            }
        }

        // Still here? Start with an empty path.
        path = NavigationPath()
    }

    func save() {
        guard let representation = path.codable else { return }

        do {
            let data = try JSONEncoder().encode(representation)
            try data.write(to: savePath)
        } catch {
            print("Failed to save navigation data")
        }
    }
}

Artık SwiftUI kodumuzu normal şekilde yazabilir ve NavigationStack’imizin path’ini bir PathStore instance’ının path property’sine bind ettiğimizden emin olabiliriz. Bu sayede rastgele tamsayılar eklenmiş view’lar gösterebiliriz, istediğimiz kadar view gönderebilir, ardından uygulamayı tam olarak bıraktığımız gibi geri almak için sessizce yeniden başlatabiliriz.

struct DetailView: View {
    var number: Int

    var body: some View {
        NavigationLink("Go to Random Number", value: Int.random(in: 1...1000))
            .navigationTitle("Number: \(number)")
    }
}

struct ContentView: View {
    @State private var pathStore = PathStore()

    var body: some View {
        NavigationStack(path: $pathStore.path) {
            DetailView(number: 0)
                .navigationDestination(for: Int.self) { i in
                    DetailView(number: i)
                }
        }
    }
}

Navigation Link Save Path


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 44 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.